package org.dicr.radius.dict;

import java.io.*;
import java.lang.ref.*;
import java.net.*;
import java.text.*;
import java.util.*;

import org.dicr.radius.*;
import org.slf4j.*;

/**
 * Implementation of {@link RadiusDictionary} with FreeRadius data source.<br/>
 * Each vendor data cached in memory with {@link SoftReference}.<br/>
 * Default location of xml is {@link #URL_DEFAULT}.
 *
 * @author <a href="http://dicr.org/">Igor (Dicr) Tarasov</a> {@literal <develop@dicr.org>}
 * @version 141026
 */
public final class FreeRadiusDictionary extends AbstractDictionary {

	/** Default dictionary resource name */
	public static final String URL_DEFAULT = "dictionary.txt";

	/** Logger */
	private static final Logger log = LoggerFactory.getLogger(FreeRadiusDictionary.class);

	/**
	 * Constructor
	 */
	private FreeRadiusDictionary(final URL url) {
		super(url);
	}

	/**
	 * Return existing or create new dictionary instance for given URL.
	 *
	 * @param url xml-resource address
	 * @return dictionary instance
	 * @see <a href="http://www.iana.org/assignments/enterprise-numbers/enterprise-numbers">IANA Enterprise Numbers</a>
	 */
	public static AbstractDictionary getInstance(final URL url) {
		if (url == null) throw new IllegalArgumentException("null dictionary");
		AbstractDictionary dict = null;
		synchronized (AbstractDictionary.INSTANCES) {
			final Reference<AbstractDictionary> ref = AbstractDictionary.INSTANCES.get(url);
			if (ref != null) dict = ref.get();
			if (dict == null) {
				dict = new FreeRadiusDictionary(url);
				AbstractDictionary.INSTANCES.put(url, new WeakReference<>(dict));
			}
		}
		return dict;
	}

	/**
	 * Return existing or create new dictionary instance for default url
	 *
	 * @return dictionary instance
	 * @throws FileNotFoundException if default resource file not found
	 * @see #URL_DEFAULT
	 */
	public static final AbstractDictionary getInstance() throws FileNotFoundException {
		final URL url = FreeRadiusDictionary.class.getResource(FreeRadiusDictionary.URL_DEFAULT);
		if (url == null) throw new FileNotFoundException(FreeRadiusDictionary.URL_DEFAULT);
		return FreeRadiusDictionary.getInstance(url);
	}

	@Override
	public final Map<Integer, VendorDescriptor> loadData() throws IOException, ParseException {
		FreeRadiusDictionary.log.debug("loading FreeRadius data from: " + this.url);
		Parser parser = new Parser();
		parser.parseFile(this.url);
		parser.processValues();
		return parser.vendors;
	}

	private static class Parser {

		@SuppressWarnings("hiding")
		private static final Logger log = LoggerFactory.getLogger(Parser.class);

		protected Map<Integer, VendorDescriptor> vendors = null;
		private Collection<String[]> defferedValues = null;

		protected Parser() {
			super();
			this.vendors = new HashMap<>();
			this.vendors.put(Integer.valueOf(0), new VendorDescriptor("RFC"));
			this.defferedValues = new ArrayList<>();
		}

		/**
		 * Parse file
		 * 
		 * @param file freeradius file to parse
		 * @throws IOException input error
		 */
		@SuppressWarnings("null")
		protected void parseFile(final URL file) throws IOException {
			if (file == null) throw new IllegalArgumentException("null file");
			try (LineNumberReader in = new LineNumberReader(new InputStreamReader(file.openStream()))) {
				Integer currVendor = Integer.valueOf(AbstractDictionary.VENDOR_NONE);
				while (true)
					try {
						String line = in.readLine();
						if (line == null) break;
						final int pos = line.indexOf('#');
						if (pos >= 0) line = line.substring(0, pos).trim();
						if (line.isEmpty()) continue;
						final String[] parts = line.split("\\s+");

						Integer code = null;
						String name = null;
						ValueType type = null;
						String enc = null;
						Long data = null;

						switch (parts[0].toUpperCase()) {
							case "$INCLUDE":
								if (parts.length != 2) throw new ParseException("INCLUDE format error", 0);
								this.parseFile(new URL(file, parts[1]));
								break;

							case "VENDOR":
								// TODO: <a href="http://freeradius.org/radiusd/man/dictionary.html">[format=t,l]</a>
								if (parts.length < 3 || parts.length > 4) throw new ParseException("VENDOR format error", 0);
								name = parts[1];
								code = Integer.valueOf(parts[2]);
								if (code.intValue() < 0 || code.intValue() > AbstractDictionary.VENDOR_MAX) throw new ParseException(
										"vendor code=" + code, 0);
								this.vendors.put(code, new VendorDescriptor(name));
								break;

							case "BEGIN-VENDOR":
								currVendor = Integer.valueOf(AbstractDictionary.VENDOR_NONE);
								if (parts.length != 2) throw new ParseException("BEGIN-VENDOR format error", 0);
								name = parts[1];
								for (final Integer key : this.vendors.keySet()) {
									final VendorDescriptor vnd = this.vendors.get(key);
									if (vnd.name.equalsIgnoreCase(name)) {
										code = key;
										break;
									}
								}
								if (code == null) throw new ParseException("vendor not found name=" + name, 0);
								currVendor = code;
								break;

							case "END-VENDOR":
								Integer oldVendor = currVendor;
								currVendor = Integer.valueOf(AbstractDictionary.VENDOR_NONE);
								if (parts.length != 2) throw new ParseException("END-VENDOR format error", 0);
								name = parts[1];
								if (!this.vendors.get(oldVendor).name.equals(name)) throw new ParseException("invalid end vendor name="
										+ name, 0);
								break;

							case "ATTRIBUTE":
								if (parts.length < 4 || parts.length > 5) throw new ParseException("ATTRIBUTE format error", 0);
								name = parts[1];
								// TODO: <a href="https://tools.ietf.org/html/rfc6929#section-2.3">TLV Data Type</a>
								code = Integer.decode(parts[2]);
								if (code.intValue() < 0
										|| (currVendor.intValue() == AbstractDictionary.VENDOR_NONE && code.intValue() > AbstractDictionary.CODE_MAX)) throw new ParseException(
										"attribute code=" + code, 0);
								type = ValueType.fromString(parts[3]);
								if (type == null) {
									Parser.log.warn("unknown attribute type=" + parts[3]);
									type = ValueType.OCTETS;
								}
								enc = parts.length > 4 ? parts[4] : null;
								this.vendors.get(currVendor).attributes.put(name, new AttDescriptor(currVendor.intValue(), code.intValue(),
										name, type, enc));
								break;

							case "VALUE":
								if (parts.length != 4) throw new ParseException("VALUE format error", 0);
								String aname = parts[1];
								name = parts[2];
								data = Long.decode(parts[3]);
								if ((data.longValue() & 0x0FFFFFFFF) != data.longValue()) throw new ParseException("invalid value data="
										+ data, 0);
								AttDescriptor attr = this.vendors.get(currVendor).attributes.get(aname);
								if (attr != null) attr.addValue(name, data.longValue());
								else this.defferedValues.add(parts);
								break;

							default:
								throw new ParseException("invalid tag '" + parts[0], 0);
						}
					} catch (Exception ex) {
						Parser.log.warn("error parsing " + file + ": " + in.getLineNumber(), ex);
					}
			}
		}

		/** Process attributes values after all files is parsed */
		protected void processValues() {
			for (final String[] parts : this.defferedValues) {
				String aname = parts[1];
				String vname = parts[2];
				Long data = Long.decode(parts[3]);
				AttDescriptor attr = null;
				for (VendorDescriptor currVendor : this.vendors.values()) {
					attr = currVendor.attributes.get(aname);
					if (attr != null) break;
				}
				if (attr != null) attr.addValue(vname, data.longValue());
				else Parser.log.warn("not found attribute name=" + aname + " for value " + vname);
			}
			this.defferedValues.clear();
		}
	}

	@Override
	public synchronized void exportData(OutputStream ou) throws IOException {
		if (ou == null) throw new IllegalArgumentException("null out");
		Map<Integer, VendorDescriptor> data = this.buildExportData();

		try (PrintStream out = new PrintStream(ou);) {
			out.println("# -*- text -*-");
			out.println("#");
			out.println("# FreeRadius dictionary exported " + DateFormat.getDateInstance().format(new Date()) + " by "
					+ this.getClass().getName());
			out.println();
			int attrCounter = 0;
			for (Integer vcode : new TreeSet<>(data.keySet())) {
				VendorDescriptor vendor = data.get(vcode);
				out.println("VENDOR\t" + vendor.name + "\t" + vcode);
				out.println();
				out.println("BEGIN-VENDOR\t" + vendor.name);
				for (AttDescriptor attr : new TreeSet<>(vendor.attributes.values())) {
					out.println("ATTRIBUTE\t" + attr.getName() + "\t" + attr.getCode() + "\t" + attr.getType().toString()
							+ (attr.getEncoding() != null ? "\t" + attr.getEncoding() : ""));
					SortedSet<Map.Entry<String, Long>> values = new TreeSet<>(new Comparator<Map.Entry<String, Long>>() {
						@Override
						public int compare(Map.Entry<String, Long> e1, Map.Entry<String, Long> e2) {
							return e1.getValue().compareTo(e2.getValue());
						}
					});
					values.addAll(attr.listValues().entrySet());					
					for (Map.Entry<String, Long> entry : values) {
						out.println("VALUE\t" + attr.getName() + "\t" + entry.getKey() + "\t" + entry.getValue());
					}
					attrCounter++;
				}
				out.println("END-VENDOR\t" + vendor.name);
				out.println();
			}
			out.flush();
			FreeRadiusDictionary.log.debug("exported " + data.size() + " vendors, " + attrCounter + " attributes");
		}
	}
}
